-- Code based on https://github.com/stein197/lua-string
-- and VERSION synchronization with this project
-- Author: chleniang@163.com

local _M = {}
_M.VERSION = "1.2.1"

local boolvalues = {
    ["1"] = "0",
    ["true"] = "false",
    ["on"] = "off",
    ["yes"] = "no",
    ["y"] = "n"
}
local eschars = {
    "\"", "'", "\\"
}
local escregexchars = {
    "(", ")", ".", "%", "+", "-", "*", "?", "[", "]", "^", "$"
}

local function includes(tbl, item)
    for k, v in pairs(tbl) do
        if v == item then
            return true
        end
    end
    return false
end


--- Splits the string by supplied separator. If the `pattern` parameter is set to true then the separator is considered
--- as a pattern.
--- @param str string string stack
--- @param sep string Separator by which separate the string.
--- @param pattern? boolean `true` for separator to be considered as a pattern. `false` by default.
--- @return string[] t Table of substrings separated by `sep` string.
local function _split(str, sep, pattern)
    if sep == "" then
        return _M.totable(str)
    end
    local rs = {}
    local previdx = 1
    while true do
        local startidx, endidx = str:find(sep, previdx, not pattern)
        if not startidx then
            table.insert(rs, str:sub(previdx))
            break
        end
        table.insert(rs, str:sub(previdx, startidx - 1))
        previdx = endidx + 1
    end
    return rs
end
_M.split = _split

--- Trims string's characters from its left side. Trims whitespaces by default. The `chars` argument is a pattern string
--- containing which characters to trim
--- @param str string string stack
--- @param chars? string Pattern that represents which characters to trim from the start. Whitespaces by default.
--- @return string s String with trimmed characters at the start.
local function _trimstart(str, chars)
    return str:gsub("^[" .. (chars or "%s") .. "]+", "")
end
_M.trimstart = _trimstart

--- Trims string's characters from its right side. Trims whitespaces by default. The `chars` argument is a pattern
--- string containing which characters to trim.
--- @param str string string stack
--- @param chars? string Pattern that represents Which characters to trim from the end. Whitespaces by default.
--- @return string s String with trimmed characters at the end.
local function _trimend(str, chars)
    return str:gsub("[" .. (chars or "%s") .. "]+$", "")
end
_M.trimend = _trimend

--- Trims string's characters from its endings. Trims whitespaces by default. The `chars` argument is a pattern
--- containing which characters to trim.
--- @param str string string stack
--- @param chars? string Pattern that represents which characters to trim from the ends. Whitespaces by default.
--- @return string s String with trimmed characters on both sides.
local function _trim(str, chars)
    chars = chars or "%s"
    return _M.trimend(_M.trimstart(str, chars), chars)
end
_M.trim = _trim

--- Adds backslashes before `"`, `'` and `\` characters.
--- @param str string string stack
--- @param eschar? string Escape character. `\` by default.
--- @param eschartbl? string[] Characters to escape. `{"\"", "'", "\\"}` by default.
--- @return string s String with escaped characters.
local function _esc(str, eschar, eschartbl)
    local s = ""
    eschar = eschar or "\\"
    eschartbl = eschartbl or eschars
    for char in _M.iter(str) do
        s = includes(eschartbl, char) and s .. eschar .. char or s .. char
    end
    return s
end
_M.esc = _esc

--- Strips backslashes from the string.
--- @param str string string stack
--- @param eschar? string Escape character. `\` by default.
--- @return string s Unescaped string with stripped escape character.
local function _unesc(str, eschar)
    local s = ""
    local i = 0
    eschar = eschar or "\\"
    while i <= #str do
        local char = str:sub(i, i)
        if char == eschar then
            i = i + 1
            s = s .. str:sub(i, i)
        else
            s = s .. char
        end
        i = i + 1
    end
    return s
end
_M.unesc = _unesc

--- Escapes pattern special characters so the string can be used in pattern matching functions as is.
--- @param str string string stack
--- @return string s String with escaped pattern special characters.
local function _escpattern(str)
    return _M.esc(str, "%", escregexchars)
end
_M.escpattern = _escpattern

--- Unescapes pattern special characters.
--- @param str string string stack
--- @return string s Unescaped string with stripped pattern `%` escape character.
local function _unescpattern(str)
    return _M.unesc(str, "%")
end
_M.unescpattern = _unescpattern

--- Pads the string at the start with specified string until specified length.
--- @param str_stack string string stack
--- @param len number To which length pad the string.
--- @param str? string String to pad the string with. " " by default
--- @return string s Padded string or the string itself if this parameter is less than string's length.
local function _padstart(str_stack, len, str)
    str = str or " "
    local selflen = str_stack:len()
    return (str:rep(math.ceil((len - selflen) / str:len())) .. str_stack):sub(-(selflen < len and len or selflen))
end
_M.padstart = _padstart

--- Pads the string at the end with specified string until specified length.
--- @param str_stack string string stack
--- @param len number To which length pad the string.
--- @param str? string String to pad the string with. " " by default
--- @return string s Padded string or the string itself if this parameter is less than string's length.
local function _padend(str_stack, len, str)
    str = str or " "
    local selflen = str_stack:len()
    return (str_stack .. str:rep(math.ceil((len - selflen) / str:len()))):sub(1, selflen < len and len or selflen)
end
_M.padend = _padend

--- Checks if the string is empty.
--- @param str string string stack
--- @return boolean b `true` if the string's length is 0.
local function _isempty(str)
    return str:len() == 0
end
_M.isempty = _isempty

--- Checks if the string consists of whitespace characters.
--- @param str string string stack
--- @return boolean b `true` if the string consists of whitespaces or it's empty.
local function _isblank(str)
    return str:match("^%s*$") ~= nil
end
_M.isblank = _isblank

--- Returns true if the string starts with specified string.
--- @param str string string stack
--- @param prefix string String to test that this string starts with.
--- @return boolean b `true` if the string starts with the specified prefix.
local function _startswith(str, prefix)
    -- return self:sub(0, prefix:len()) == prefix
    return (str:find(prefix, 1, true) == 1)
end
_M.startswith = _startswith

--- Returns true if the string ends with specified string.
--- @param str string string stack
--- @param suffix string String to test that this string ends with.
--- @return boolean b `true` if the string ends with the specified suffix.
local function _endswith(str, suffix)
    -- return self:sub(self:len() - suffix:len() + 1) == suffix
    return (str:sub(-1 * string.len(suffix)) == suffix)
end
_M.endswith = _endswith

--- If the string starts with specified prefix then returns string itself, otherwise pads the string until it starts
--- with the prefix.
--- @param str string string stack
--- @param prefix string String to ensure this string starts with.
--- @return string s String that starts with specified prefix.
local _ensurestart = function(str, prefix)
    local prefixlen = prefix:len()
    if prefixlen > str:len() then
        return _M.ensureend(prefix, str)
    end
    local left = str:sub(1, prefixlen)
    local i = 1
    while not _M.endswith(prefix, left) and i <= prefixlen do
        i = i + 1
        left = left:sub(1, -2)
    end
    return prefix:sub(1, i - 1) .. str
end
_M.endswith = _ensurestart

--- If the string ends with specified suffix then returns string itself, otherwise pads the string until it ends with
--- the suffix.
--- @param str string string stack
--- @param suffix string String to ensure this string ends with.
--- @return string s String that ends with specified prefix.
local function _ensureend(str, suffix)
    local suffixlen = suffix:len()
    if suffixlen > str:len() then
        return _M.ensurestart(suffix, str)
    end
    local right = str:sub(-1 * suffixlen)
    local i = suffixlen
    while not _M.startswith(suffix, right) and i >= 1 do
        i = i - 1
        right = right:sub(2)
    end
    return str .. suffix:sub(i + 1)
end
_M.endswith = _ensureend

--- Returns an iterator which can be used in `for ... in` loops.
--- @param str string string stack
--- @return fun(): string f Iterator.
local function _iter(str)
    return str:gmatch(".")
end
_M.iter = _iter

--- Converts "1", "true", "on", "yes", "y" and their contraries into real boolean. Case-insensetive.
--- @param str string string stack
--- @return boolean | nil b Boolean corresponding to the string or nil if casting cannot be done.
local function _tobool(str)
    local lowered = str:lower()
    for truthy, falsy in pairs(boolvalues) do
        if lowered == truthy then
            return true
        elseif lowered == falsy then
            return false
        end
    end
    return nil
end
_M.tobool = _tobool

--- Returns table containing all the chars in the string.
--- @param str string string stack
--- @return string[] t Table that consists of the string's characters.
local function _totable(str)
    local result = {}
    for ch in _M.iter(str) do
        table.insert(result, ch)
    end
    return result
end
_M.totable = _totable

return _M
